<!--
* @Description 大文件上传 FileUploadIndex
* @author 三咲问道
* @date 2025/6/4
-->
<template>
  <div class="FileUploadIndex">
    <div>大文件上传</div>
    <div>日常业务中难免出现前端需要向后端传输大型文件的情况，这时单次的请求不能满足传输大文件的需求，就需要用到分片上传
    </div>
    <div>业务需求为：用户可以上传小于20G的镜像文件，并进显示当前上传进度</div>
    <div>前端：vue3.x+Element Plus组件+axios</div>
    <div>
      解决思路简单为前端选择文件后读取到文件的基本信息，包括：文件的大小、文件格式等信息，用于前端校验，校验完成后将文件进行切片并通过请求轮询把切片传递给后端
    </div>
    <div class="upload">
      <el-upload :show-file-list="false" action="" :http-request="putinMirror">
        <el-button type="primary" :icon="UploadFilled">上传</el-button>
      </el-upload>
      <!--  进度条  -->
      <el-progress :percentage="progress" :indeterminate="true"/>
    </div>

  </div>

</template>

<script setup lang="ts">
import {ref} from 'vue'
import {ElMessage} from 'element-plus'
import {UploadFilled} from '@element-plus/icons-vue'

const progress = ref(0)

/* 上传环境镜像 分片上传 */
const putinMirror = async (file) => {
  // 校验文件是否符合规范（注意这里的异步方法，因为调用了接口加上await，校验函数若不调用接口可以不写await，否则返回promise对象）
  if (await checkMirrorFile(file)) {
    // 文件相关信息
    let files = file.file
    // 从0开始的切片
    let shardIndex = 0
    // 调用切片方法
    uploadFile(files, shardIndex)
  }
}

// 2.校验文件是否符合规范
// 这一步可以根据需求来进行校验，这里需要通过接口校验当前服务器可用的磁盘容量来判断是否有足够的空间用于存放将要上传的文件

/* 校验上传镜像文件是否符合规范 */
const checkMirrorFile = async (file) => {
  // 校验文件格式是否正确，支持.acow2/.iso/.ovf/.zip/.tar
  let fileType = file.file.name.split('.')
  if (fileType[fileType.length - 1] !== 'acow2' && fileType[fileType.length - 1] !== 'iso' && fileType[fileType.length - 1] !== 'ovf' && fileType[fileType.length - 1] !== 'zip' && fileType[fileType.length - 1] !== 'tar') {
    ElMessage.warning('文件格式错误，仅支持.acow2/.iso/.ovf/.zip/.tar')
    return false
  }

  // 校验文件大小是否满足
  let fileSize = file.file.size

  //文件大小是否超出20G

  if (fileSize > 20 * 1024 * 1024 * 1024) {
    ElMessage.warning('上传文件大小不超过20G')
    return false
  }

  const res = await checkMirrorFileApi()

  if (res.code !== 200) {
    ElMessage.warning('暂时无法查看磁盘可用空间，请重试')
    return false
  }

  // 查看磁盘容量大小
  if (res.data.diskDevInfos && res.data.diskDevInfos.length > 0) {
    let saveSize = 0
    res.data.diskDevInfos.forEach(i => {
      // 磁盘空间赋值
      if (i.devName === '/dev/mapper/centos-root') {
        // 返回值为GB，转为字节B
        saveSize = i.free * 1024 * 1024 * 1024
      }
    })

    // 上传的文件大小没有超出磁盘可用空间
    if (fileSize < saveSize) {
      return true
    } else {
      ElMessage.warning('文件大小超出磁盘可用空间容量')
      return false
    }
  } else {
    ElMessage.warning('文件大小超出磁盘可用空间容量')

    return false
  }

}

// 3.文件切片上传
// 校验完成后就可以进行文件的切片上传了，这里用的类似接口轮询的方式，每次携带一个切片信息给后端，后端接受到切片并返回成功状态码后再进行下一次切片的上传，代码如下：
// 当然这里后端没有过多的做切片的处理，可以通过前端使用多线程，或者不等接口响应成功就进行下一次传递切片的过程进行上传的提速，这里具体怎么实现看业务需求或者怎么配合上传

/* 文件切片上传 */
const uploadFile = async (file, shardIndex, createTime, savePath, relativePath, timeMillis) => {
  // 文件名
  let name = file.name
  // 文件大小
  let size = file.size
  // 分片大小
  let shardSize = 1024 * 1024 * 5
  // 分片总数
  let shardTotal = Math.ceil(size / shardSize)
  if (shardIndex >= shardTotal) {
    isAlive.value = false
    progress.value = 100
    return
  }

  // 文件开始结束的位置
  let start = shardIndex * shardSize
  let end = Math.min(start + shardSize, size)

  // 开始切割
  let packet = file.slice(start, end)
  let formData = new FormData()
  formData.append("file", packet)
  formData.append("fileName", name)
  formData.append("size", size)
  formData.append("shardIndex", shardIndex)
  formData.append("shardSize", shardSize)
  formData.append("shardTotal", shardTotal)

  // 下面这些值是后端组装切片时需要的，跟前端关系不大

  if (createTime) formData.append("createTime", createTime)
  if (savePath) formData.append("savePath", savePath)
  if (shardIndex < shardTotal) {
    // 进度条保留两位小数展示
    progress.value = ((shardIndex / shardTotal) * 100).toFixed(2) * 1
    const res = await uploadShardFileApi(formData)
    if (res.code !== 200) {
      ElMessage.error(res.msg)
      progress.value = 0
      return
    }

    if (res.msg == '上传成功' && res.data.fileName && res.data.filePath) {
      // 这里为所有切片上传成功后进行的操作
      console.log('所有切片上传成功')
    }

    shardIndex++
    uploadFile(file, shardIndex, res.data.createTime, res.data.savePath, res.data.relativePath, res.data.timeMillis)
  }
}

</script>

<style scoped lang="less">
.FileUploadIndex {
  .upload {
    margin-top: 12px;
  }

  .icon {
    display: inline-block;
    width: 24px;
    height: 24px;
  }
}
</style>